// #[macro_use]
extern crate log;
extern crate colog;

extern crate walkdir;
extern crate md5;
extern crate dotext;
extern crate lopdf;

extern crate threadpool;
extern crate dirs;

use log::{info, warn, error};
use std::error::Error as StdError;
// use dirs;
use std::sync::mpsc::channel;
use std::sync::mpsc::{Sender, Receiver};
use walkdir::WalkDir;
use lopdf::Document;
// use std::env;
use std::path::Path;
use std;
use std::fmt;
use md5::compute;
use std::io::Read;
use std::io::prelude::*;
use std::fs::File;
// use colored::Colorize;
// use std::time::Instant;

// use pdf_extract::extract_text;

use threadpool::ThreadPool;

use dotext::*;
use doc::{OpenOfficeDoc, MsDoc};
use String;
use std::time::SystemTime;
use std::io;
use std::io::{Error, ErrorKind};

use super::cmd::{get_pdf_text, use_pdftotext};

// use mongodb::coll::options::WriteModel;
// use mongodb::coll::Collection;

use mongodb::{bson, doc};
use mongodb::{Client,ThreadedClient, Bson};
use mongodb::db::ThreadedDatabase;

use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use std::panic;
// use log::warn;


pub static mut DISPLAY_MORE : bool = false;

type Out<T> = Result<T, Box<dyn StdError>>;


#[derive(Default)]
pub struct Text {
    pub id : String,
    pub content : String,
    pub path : String,
    // no : i32,
}

#[derive(Serialize, Deserialize, Debug)]
struct Record {
    name : String,
    value : i64,
    info_type : String,
    
}

fn deleted_records(deleted_keys: &Vec<String>) {
    println!("deleted function : delted [{}]", deleted_keys.len());
    let client = Client::connect("localhost", 27017).expect("Failed to initialize client.");
    let db = client.db("sbas");
    let coll = db.collection("meta");
    for key in deleted_keys.iter() {
        coll.delete_one(doc!{
            "name" :key,
            "info_type" : "record"
        }, None).unwrap();
    }
}

pub fn update_last_record(old_map : &HashMap<String,i64> , record_map: HashMap<String,i64> ) {
    let client = Client::connect("localhost", 27017).expect("Failed to initialize client.");
    let db = client.db("sbas");
    let coll = db.collection("meta");
    for (name, value) in &record_map{
        let filter = doc!{
            "name" => name
        };
        if old_map.contains_key(name){
            let update_doc = doc!{ 
                "$set": { 
                    "name" => name,
                    "value" => *value,
                    "info_type" => "record"
                }
            };
            coll.update_one(filter, update_doc, None).expect("upsert to mongo db failed !");
        }else{
            let doc = doc!{ 
                "name" => name,
                "value" => *value,
                "info_type" => "record"
            
            };
            coll.insert_one(doc,None).expect("insert failed");
        }

        
        
        
    }
    // Execute InsertOne operations, ordered.
}

pub fn if_valid_tp(path: &Path) -> bool{
    if let Some(tp) = path.to_str().expect("[329]").split(".").last(){
        return match tp {
            "pdf" => true,
            "docx" => true,
            "txt" => true,
            _ => false
        }
    }
    true
}


pub fn get_last_record() -> Result<HashMap<String,i64>, &'static str> {
    let client = Client::connect("localhost", 27017).expect("Failed to initialize client.");
    let db = client.db("sbas");
    let coll = db.collection("meta");
    let mut record_map :HashMap<String,i64> = HashMap::new();
    let cursor = match coll.find(Some(doc!{"info_type" => "record"}), None){
        Ok(t) => t,
        Err(e) => panic!(e)
    };
    // let doc = cursor.next()//.expect("not found bson");
    for result in cursor {
        if let Ok(item) = result {
            if let Some(&Bson::String(ref name)) = item.get("name") {
                if let Some(&Bson::I64(ref value)) = item.get("value"){
                    record_map.entry(name.to_string()).or_insert(*value);
                }
            }
        }
        // return Err("Not found in mongo db");
    }

    return Ok(record_map);
}

pub fn get_now() -> i64{
    SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64
}

pub fn get_file_last_change_time(file: &str) -> io::Result<(String, i64)> {
    
    let metadata = match std::fs::metadata(file){
        Ok(s) => s,
        Err(e) => {
            println!("metadata feild for file{}", file);
            return Err(e);
        }
    };

    if let Ok(time) = metadata.modified() {
        let time_secs = time.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
        let file_path = Path::new(file);
        return Ok((file_path.canonicalize().unwrap().to_str().unwrap().to_string(), time_secs));
    } else {
        println!("Not supported on this platform");
    }
    return Err(Error::new(ErrorKind::Other, "oh no! in get file last changed time"));
}

pub fn to_line_file( list:&Vec<String>, file_path: Option<&str>) -> io::Result<()> {
    if let Some(path) = file_path {
        let mut file = File::create(path)?;
        let content = list.join("\n");
        file.write_all((&content).as_bytes())?;
    }else{
        let dir_path = Path::new("/tmp");
        let file_end = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs().to_string() + ".deleted";
            
            // let file_end = time_end.as_secs().to_string() + ".deleted";
        if let Some(path) = dir_path.join(file_end).to_str(){
            let mut file = File::create(path)?;
            let content = list.join("\n");
            file.write_all((&content).as_bytes())?;
        }
    }
    
    Ok(())
} 

fn extract_txt(path:&Path) -> Out<String> {
    if use_pdftotext(){
        Ok(get_pdf_text(path)?)
    }else{
        let doc = Document::load(path)?;
        let nums:Vec<u32> = doc.get_pages().keys().map(|&k| k).collect();
        Ok(doc.extract_text(nums.as_slice())?)   
    }
    
}


impl Text {
    fn build(content:&str, name:&str) -> Text{
        let digest = compute(name.as_bytes());
        let t = Text{id: format!("{:x}",digest), content: String::from(content.clone()),..Text::default()};
        return t;
    }
    fn form_pdf(f_name:&str ) -> Result<Text, &'static str> {
        
        let path = Path::new(f_name);
        if path.is_file(){
            

            // let mut err_test = false;
            
            let all_text = match panic::catch_unwind(||{
                extract_txt(path)
            }){
                Ok(t) => {
                    match t {
                        Ok(txt) => txt,
                        _ => {return Err("extract failed");}
                    }
                },
                Err(_) => {return Err("extract failed");}
            };
            
            // let start_t = Instant::now();
            let mut text =  Text::build(&all_text, f_name);
            // warn!("used {:?} deal pdf: {}", start_t.elapsed().as_secs(), path.file_name().expect("to str err").to_str().unwrap());
            text.path = path.canonicalize().expect("trans to abs path failed").to_str().expect("trans to str failed").to_string();
            
            return Ok(text);
        }
        return Err("file not exists!");
    }

    fn form_txt(f_name:&str) -> Result<Text, &'static str> {
        let path = Path::new(f_name);
        let mut buf = String::new();
        let mut file = File::open(&path).expect("open failed");
        match file.read_to_string(&mut buf){
            Ok(_) => {
                let mut t:Text = Text::build(&mut buf, f_name);
                t.path = path.canonicalize().unwrap().to_str().unwrap().to_string();
                Ok(t)
            },
            _ => {Err("read txt failed")}
        }
        
    }

    fn from_office(f_name:&str, file_type:&str) -> Result<Text, &'static str>{
        let path = Path::new(f_name);
        
        if path.is_file(){
            let mut isi = String::new();
            match file_type {
                "docx" =>{
                    let mut file =  match Docx::open(path){
                        Ok(f) => f,
                        Err(_) => {
                            error!("file open err :{}", path.display());
                            return Err("failed in thread open");
                        }
                    };
                    let _ = file.read_to_string(&mut isi);
                },
                // "doc" => {
                //     let mut file = Docx::open(path).expect("Cannot open file");
                //     let _ = file.read_to_string(&mut isi);
                // },
                "odp" => {
                    let mut file = Odp::open(path.to_str().unwrap()).expect("Cannot open file");
                    let _ = file.read_to_string(&mut isi);
                },
                "ods" => {
                    let mut file = Ods::open(path).expect("Cannot open file");
                    let _ = file.read_to_string(&mut isi);

                },
                "odt" => {
                    let mut file = Odt::open(path).expect("Cannot open file");
                    let _ = file.read_to_string(&mut isi);

                },
                "xlsx" => {
                    let mut file = Xlsx::open(path.to_str().unwrap()).expect("Cannot open file");
                    let _ = file.read_to_string(&mut isi);

                },
                "pptx" => {
                    let mut file = Pptx::open(path.to_str().unwrap()).expect("Cannot open file");
                    let _ = file.read_to_string(&mut isi);
                },
                _ => {
                    return Err("not office file!");
                }
            };
            

            let mut t:Text = Text::build(&mut isi, f_name);
            t.path = path.canonicalize()
                .expect("file not exists this moment")
                .to_str()
                .expect("file not exists this moment").to_string();
            return Ok(t);
        }else{
            println!("not file {}",f_name);
            return Err("no doc");
        }
    }

    pub fn to_file(&self, dir:&Path) -> Result<bool, &'static str>{
        let path = dir.join(self.id.clone() + ".txt");
        let mut file = File::create(path).unwrap();
        match file.write_all(( format!("{}\n", &self.path)).as_bytes()){
            Ok(_) => {},
            Err(_) => {
                return Err("not write good");
            }
        };
        match file.write_all(&self.content.as_bytes()){
            Ok(_) => {return Ok(true);},
            Err(_) => {return Err("not write content good!");}
        }
    }

    pub fn from_file(f_name:&str) -> Result<Text, &'static str> {
        let file_end:&str = f_name.split(".").last().unwrap();
        // println!("deal {}",f_name);
        match file_end {
            // "docx" | "pptx" | "odp" | "ods" | "odt" |"xlsx" => Text::from_office(f_name, file_end),
            "docx" | "odp" | "ods" | "odt" |"xlsx" => Text::from_office(f_name, file_end),
            "pdf" => Text::form_pdf(f_name),
            "txt" => Text::form_txt(f_name),
            _ => {
                Err("")
            },
        }
    }

    // pub fn get_no(&self) -> i32 {
    //     return self.no;
    // } 

// after : Option<&Fn(Sender<Result<Text, &'static str>>)>

    #[warn(unused_variables)]
    pub fn  scan<'a>(paths: Vec<String>, send: Sender<Result<Text, &'static str>>, _thread_num:Option<i32>) -> (i32, ThreadPool, Vec<String>) {
        let mut count = 0;
        // let mut clog = colog::builder();
        // clog.filter(None, log::LevelFilter::Warn);
        // clog.init();
        let thread_num:usize = match _thread_num {
            Some(expr) => expr as usize,
            None => 30,
        };
        
        let pool = ThreadPool::with_name("worker".into(), thread_num);
        warn!("thread num : {}", thread_num);
        let files_records = match get_last_record(){
            Ok(t) => {t},
            Err(e) => {
                println!("err happend in get last record");
                panic!(e);
            },
        };

        let mut new_records: HashMap<String, i64> = HashMap::new();
        let mut used_keys:Vec<String> = vec![];
        // let mut path2 = "";
        let home = dirs::home_dir().expect("call home_dir error ");
        // let mut dir_paths:Vec<&Path> = vec![];
        let mut dir_paths: Vec<String> = paths.iter().map(|p|{
            // println!("path : [{}]",p);
            if p.contains("\""){
                p.replace("\"", "")
            }else{
                p.to_string()
            }
        }).collect();
        dir_paths = dir_paths.iter().map(| p|{
            if p.starts_with("~/"){
                let (_, last) = p.split_at(2);
                format!("{}/{}",home.to_str().expect("get home str error"),last)
            }else{
                p.to_string()
            }
        }).collect();
        
        for path_dir_str in &dir_paths{
            let  path_dir = Path::new(path_dir_str);
            
            // println!("scan in : {}",path_dir.display());
            let mut jump_dir  = String::from("");  
            for entry in WalkDir::new(&path_dir) {
                // println!(" ..... deal : {}",entry.unwrap().path()to_str().unwrap());
                let entry = match entry {
                    Ok(t) => t,
                    Err(e) => {
                        error!("{} err!", path_dir.display());
                        panic!(e);
                        // continue;
                    },
                };
                
                if ! if_valid_tp(entry.path()){
                    continue;
                }
                
                if entry.path().to_str().unwrap().contains(&jump_dir) && jump_dir != ""{
                    continue;
                }
                
                if let Some(ppp) = entry.path().to_str(){
                    if cfg!(target_os = "windows") {
                        if ppp.contains("\\."){
                            if entry.file_type().is_dir(){
                                jump_dir = ppp.to_string();
                            }
                            continue;
                        }
                         
                    }else{
                        if ppp.contains("/."){
                            if entry.file_type().is_dir(){
                                jump_dir = ppp.to_string();
                            }
                            // warn!("jump {}", ppp.blue());
                            continue;
                        }else{
                            // warn!("test {}", ppp.blue());
                        }
                    };
                }
                

                

                if entry.file_type().is_dir() {
                    info!("now in dir : {}", entry.path().display());
                    continue;
                }
                let (finsert,t ) = get_file_last_change_time(entry.path().to_str().expect("entry to str error in [370]")).expect("get file changed time error");
                
                used_keys.push(finsert.clone());

                if files_records.contains_key(&finsert){
                    if let Some(t2) = files_records.get(&finsert){
                        if *t2 == t {
                            warn!("already {}", finsert);
                            continue
                        }
                    }
                }
                let tx = send.clone();
                
                
                // let r = thread::catch_panic(|| {
                //     thread::sleep_ms(500);
                //     panic!(String::from("boom again!"));
                // });
                let f= entry.path().to_str().expect("path canot to str !");
                
                if let Some(a) = f.split(".").last(){
                    match a {
                        "pdf" | "docx" | "pptx"| "txt" => {
                            new_records.insert(finsert, t);
                            count += 1;
                            pool.execute(move|| {
                            // let start = Instant::now();
                            // println!("{} {} ", thread::current().name().unwrap(),entry.path().to_str().unwrap());
                                let f= entry.path().to_str().expect("path canot to str !");
                                
                                match Text::from_file(f) {
                                    Ok(tt) => {
                                        // println!("{} -> channel", &tt.path);
                                        // warn!("dealed -> {}", f.green());
                                        tx.send(Ok(tt)).expect(" channel will be waiting for pool")
                                    },
                                    Err(_) => {
                                        // warn!("dealed warn -> {}", f.yellow());
                                        tx.send(Ok(Text{
                                            id:"-1".to_string(),
                                            path: f.to_string(),
                                            content:"-1".to_string(),
                                            // no:count,
                                        })).expect("send result error");

                                    }
                                }
                            });
                        },
                        _ => {continue},
                    }
                }
            }
        }
        
        
        if new_records.is_empty(){

        }else{
            update_last_record(&files_records, new_records);
        }
        
        let mut deleted_keys:Vec<String> = vec![];
        
        for (key, _) in &files_records{
            if ! used_keys.contains(&key){
                deleted_keys.push(key.to_string());
            }
        };
        
        if deleted_keys.len() > 0{
            deleted_records(&deleted_keys);
        }
        
        
        send.send(Err(Box::leak(format!("{}", count).into_boxed_str()) )).unwrap();
        
        
        
        return (count, pool, deleted_keys);
    }
}

impl std::fmt::Display for Text{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result{
        unsafe {
            if DISPLAY_MORE {
                return write!(f, "------- {} | {} : {} -------\n {} \n -------------------", self.id, self.path, self.content.len(), self.content);
            }else{
                return write!(f, "------- {} | {} : {} -------", self.id, self.path, self.content.len());
            }
        }
    }
}

pub fn get_channels() ->  (Sender<Result<Text, &'static str>>, Receiver<Result<Text, &'static str>>) {
    return channel::<Result<Text, &'static str>>();
}